
var QWebChannelMessageTypes = {
    signal: 1,
    propertyUpdate: 2,
    init: 3,
    idle: 4,
    debug: 5,
    invokeMethod: 6,
    connectToSignal: 7,
    disconnectFromSignal: 8,
    setProperty: 9,
    response: 10,
};

class QWebChannel {
  constructor(transport, initCallback) {
    if (typeof transport !== "object" || typeof transport.send !== "function") {
      console.error(
        "The QWebChannel expects a transport object with a send function and onmessage callback property." +
          " Given is: transport: " +
          typeof transport +
          ", transport.send: " +
          typeof transport.send
      );
      return;
    }

    this.transport = transport;
    this.execCallbacks = {};
    this.execId = 0;
    this.objects = {};

    this.send = function (data) {
      if (typeof data !== "string") {
        data = JSON.stringify(data);
      }
      this.transport.send(data);
    };

    this.transport.onMessage = (message) => {
      let data = message;
      if (typeof data === "string") {
        data = JSON.parse(data);
      }

      switch (data.type) {
        case QWebChannelMessageTypes.signal:
          this.handleSignal(data);
          break;
        case QWebChannelMessageTypes.response:
          this.handleResponse(data);
          break;
        case QWebChannelMessageTypes.propertyUpdate:
          this.handlePropertyUpdate(data);
          break;
        default:
          console.error("invalid message received:", message.data);
          break;
      }
    };

    this.exec = function (data, callback) {
      if (!callback) {
        this.send(data);
        return;
      }
      if (this.execId === Number.MAX_VALUE) {
        this.execId = Number.MIN_VALUE;
      }
      if (data.hasOwnProperty("id")) {
        console.error(
          "Cannot exec message with property id: " + JSON.stringify(data)
        );
        return;
      }
      data.id = this.execId++;
      this.execCallbacks[data.id] = callback;
      this.send(data);
    };

    this.debug = function (message) {
      this.send({ type: QWebChannelMessageTypes.debug, data: message });
    };

    this.exec({ type: QWebChannelMessageTypes.init }, (data) => {
      for (let objectName in data) {
        new QObject(objectName, data[objectName], this);
      }
      for (let objectName in this.objects) {
        this.objects[objectName].unwrapProperties();
      }
      if (initCallback) {
        initCallback(this);
      }
      this.exec({ type: QWebChannelMessageTypes.idle });
    });
  }

  handleSignal(message) {
    const object = this.objects[message.object];
    if (object) {
      object.signalEmitted(message.signal, message.args);
    } else {
      console.warn(
        "Unhandled signal: " + message.object + "::" + message.signal
      );
    }
  }

  handleResponse(message) {
    if (!message.hasOwnProperty("id")) {
      console.error(
        "Invalid response message received: ",
        JSON.stringify(message)
      );
      return;
    }
    this.execCallbacks[message.id](message.data);
    delete this.execCallbacks[message.id];
  }

  handlePropertyUpdate(message) {
    for (let i in message.data) {
      const data = message.data[i];
      const object = this.objects[data.object];
      if (object) {
        object.propertyUpdate(data.signals, data.properties);
      } else {
        console.warn(
          "Unhandled property update: " + data.object + "::" + data.signal
        );
      }
    }
    this.exec({ type: QWebChannelMessageTypes.idle });
  }
}

class QObject {
  constructor(name, data, webChannel) {
    this.__id__ = name;
    webChannel.objects[name] = this;
    this.__objectSignals__ = {};
    this.__propertyCache__ = {};

    const object = this;

    this.unwrapQObject = function (response) {
      if (response instanceof Array) {
        const ret = new Array(response.length);
        for (let i = 0; i < response.length; ++i) {
          ret[i] = object.unwrapQObject(response[i]);
        }
        return ret;
      }
      if (!response || !response["__QObject*__"] || response["id"] === undefined) {
        return response;
      }

      const objectId = response.id;
      if (webChannel.objects[objectId]) return webChannel.objects[objectId];

      if (!response.data) {
        console.error(
          "Cannot unwrap unknown QObject " + objectId + " without data."
        );
        return;
      }

      const qObject = new QObject(objectId, response.data, webChannel);
      qObject.destroyed.connect(() => {
        if (webChannel.objects[objectId] === qObject) {
          delete webChannel.objects[objectId];
          const propertyNames = [];
          for (let propertyName in qObject) {
            propertyNames.push(propertyName);
          }
          for (let idx in propertyNames) {
            delete qObject[propertyNames[idx]];
          }
        }
      });
      qObject.unwrapProperties();
      return qObject;
    };

    this.unwrapProperties = function () {
      for (let propertyIdx in object.__propertyCache__) {
        object.__propertyCache__[propertyIdx] = object.unwrapQObject(
          object.__propertyCache__[propertyIdx]
        );
      }
    };

    function addSignal(signalData, isPropertyNotifySignal) {
      const signalName = signalData[0];
      const signalIndex = signalData[1];
      object[signalName] = {
        connect: function (callback) {
          if (typeof callback !== "function") {
            console.error(
              "Bad callback given to connect to signal " + signalName
            );
            return;
          }

          object.__objectSignals__[signalIndex] =
            object.__objectSignals__[signalIndex] || [];
          object.__objectSignals__[signalIndex].push(callback);

          if (!isPropertyNotifySignal && signalName !== "destroyed") {
            webChannel.exec({
              type: QWebChannelMessageTypes.connectToSignal,
              object: object.__id__,
              signal: signalIndex,
            });
          }
        },
        disconnect: function (callback) {
          if (typeof callback !== "function") {
            console.error(
              "Bad callback given to disconnect from signal " + signalName
            );
            return;
          }
          object.__objectSignals__[signalIndex] =
            object.__objectSignals__[signalIndex] || [];
          const idx = object.__objectSignals__[signalIndex].indexOf(callback);
          if (idx === -1) {
            console.error(
              "Cannot find connection of signal " +
                signalName +
                " to " +
                callback.name
            );
            return;
          }
          object.__objectSignals__[signalIndex].splice(idx, 1);
          if (
            !isPropertyNotifySignal &&
            object.__objectSignals__[signalIndex].length === 0
          ) {
            webChannel.exec({
              type: QWebChannelMessageTypes.disconnectFromSignal,
              object: object.__id__,
              signal: signalIndex,
            });
          }
        },
      };
    }

    function invokeSignalCallbacks(signalName, signalArgs) {
      const connections = object.__objectSignals__[signalName];
      if (connections) {
        connections.forEach((callback) => {
          callback.apply(callback, signalArgs);
        });
      }
    }

    this.propertyUpdate = function (signals, propertyMap) {
      for (let propertyIndex in propertyMap) {
        const propertyValue = propertyMap[propertyIndex];
        object.__propertyCache__[propertyIndex] = propertyValue;
      }

      for (let signalName in signals) {
        invokeSignalCallbacks(signalName, signals[signalName]);
      }
    };

    this.signalEmitted = function (signalName, signalArgs) {
      invokeSignalCallbacks(signalName, signalArgs);
    };

    function addMethod(methodData) {
      const methodName = methodData[0];
      const methodIdx = methodData[1];
      object[methodName] = function () {
        const args = [];
        let callback;
        for (let i = 0; i < arguments.length; ++i) {
          if (typeof arguments[i] === "function") callback = arguments[i];
          else args.push(arguments[i]);
        }

        webChannel.exec(
          {
            type: QWebChannelMessageTypes.invokeMethod,
            object: object.__id__,
            method: methodIdx,
            args: args,
          },
          (response) => {
            if (response !== undefined) {
              const result = object.unwrapQObject(response);
              if (callback) {
                callback(result);
              }
            }
          }
        );
      };
    }

    function bindGetterSetter(propertyInfo) {
      const propertyIndex = propertyInfo[0];
      const propertyName = propertyInfo[1];
      const notifySignalData = propertyInfo[2];
      object.__propertyCache__[propertyIndex] = propertyInfo[3];

      if (notifySignalData) {
        if (notifySignalData[0] === 1) {
          notifySignalData[0] = propertyName + "Changed";
        }
        addSignal(notifySignalData, true);
      }

      Object.defineProperty(object, propertyName, {
        get: function () {
          const propertyValue = object.__propertyCache__[propertyIndex];
          if (propertyValue === undefined) {
            console.warn(
              'Undefined value in property cache for property "' +
                propertyName +
                '" in object ' +
                object.__id__
            );
          }

          return propertyValue;
        },
        set: function (value) {
          if (value === undefined) {
            console.warn(
              "Property setter for " +
                propertyName +
                " called with undefined value!"
            );
            return;
          }
          object.__propertyCache__[propertyIndex] = value;
          webChannel.exec({
            type: QWebChannelMessageTypes.setProperty,
            object: object.__id__,
            property: propertyIndex,
            value: value,
          });
        },
      });
    }

    data.methods.forEach(addMethod);
    data.properties.forEach(bindGetterSetter);
    data.signals.forEach((signal) => {
      addSignal(signal, false);
    });

    for (let name in data.enums) {
      object[name] = data.enums[name];
    }
  }
}

export { QWebChannelMessageTypes, QWebChannel, QObject };